package com.snail.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.snail.constants.RabbitMqConstants;
import com.snail.constants.SystemConstants;
import com.snail.exception.BizException;
import com.snail.mapper.BizChunkRecordMapper;
import com.snail.mapper.BizFilesMapper;
import com.snail.mapper.BizMqMessageMapper;
import com.snail.model.dto.media.FileMergeDto;
import com.snail.model.dto.media.UploadFileResultDto;
import com.snail.model.pojo.media.BizChunkRecord;
import com.snail.model.pojo.media.BizFiles;
import com.snail.model.pojo.media.BizMqMessage;
import com.snail.model.vo.media.UploadFileVo;
import com.snail.result.Result;
import com.snail.result.ResultCode;
import com.snail.service.BizFilesService;
import com.snail.utils.AliYunGreenUtils;
import com.snail.utils.GreenResponse;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 * @author Monica
 * @description 针对表【biz_files(文件主表（图片、文档、视频通用）)】的数据库操作Service实现
 * @createDate 2025-08-12 16:12:54
 */
@Slf4j
@Service
public class BizFilesServiceImpl extends ServiceImpl<BizFilesMapper, BizFiles> implements BizFilesService {

    @Autowired
    private MinioClient minioClient;
    @Autowired
    private BizFilesMapper bizFilesMapper;
    @Autowired
    @Lazy
    private BizFilesService currentProxy;
    @Autowired
    private BizMqMessageMapper bizMqMessageMapper;
    @Autowired
    private AliYunGreenUtils aliYunGreenUtils;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private BizChunkRecordMapper bizChunkRecordMapper;
    //图片文件桶
    @Value("${minio.bucket.pic}")
    private String bucketPic;
    @Value("${minio.bucket.video}")
    private String bucketVideo;
    @Value("${minio.bucket.doc}")
    private String bucketDoc;
    @Value("${minio.endpoint}")
    private String bucketPoint;



    /**
     * 上传文件
     *
     * @param uploadFileParams 上传文件参数
     * @param absolutePath     绝对路径
     * @return 上传文件结果
     */
    @Override
    public Result<UploadFileResultDto> uploadFile(UploadFileVo uploadFileParams, String absolutePath) {
        File file = new File(absolutePath);
        if (!file.exists()) {
            throw new BizException(ResultCode.PARAM_IS_NULL);
        }

        //文件名称
        String filename = uploadFileParams.getFileName();
        //文件扩展名
        String extension = filename.substring(filename.lastIndexOf("."));
        //文件mimeType
        String mimeType = getMimeType(extension);
        //文件的md5值
        String fileMd5 = getFileMd5(file);
        //文件的默认目录
        String defaultFolderPath = getDefaultFolderPath();
        //存储到minio中的对象名(带目录)
        String objectName = defaultFolderPath + fileMd5 + extension;
        // 判断是上传文档还是上传图片
        String bucket = null;
        if (uploadFileParams.getFileType() == 1) {
            //上传图片
            bucket = bucketPic;
        } else if (uploadFileParams.getFileType() == 2) {
            //上传图片
            bucket = bucketDoc;
        }
        //将文件上传到minio
        boolean b = addMediaFilesToMinio(absolutePath, mimeType, bucket, objectName);
        //文件大小
        uploadFileParams.setFileSize(file.length());
        // 图片文档直接完成
        uploadFileParams.setStatus(3);
        //将文件信息存储到数据库
        BizFiles mediaFiles = currentProxy.addMediaFilesToDb(fileMd5, uploadFileParams, bucket, objectName);
//        // 审核图片
//        if (uploadFileParams.getFileType() == 1) {
//            GreenResponse greenResponse = aliYunGreenUtils.reviewVideo(bucketPoint + mediaFiles.getPreviewUrl());
//            if (greenResponse.getCode() != 200) {
//
//            }
//        }


        //准备返回数据
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
        return Result.success(uploadFileResultDto);
    }


    /**
     * @param localFilePath 文件地址
     * @param bucket        桶
     * @param objectName    对象名称
     * @return boolean
     * @description 将文件写入minIO
     */
    public boolean addMediaFilesToMinio(String localFilePath, String mimeType, String bucket, String objectName) {
        try {
            UploadObjectArgs objectArgs = UploadObjectArgs.builder().bucket(bucket).object(objectName).filename(localFilePath).contentType(mimeType).build();

            minioClient.uploadObject(objectArgs);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            return true;
        } catch (Exception e) {
            log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}", bucket, objectName, e.getMessage(), e);
        }
        return false;
    }

    /**
     * @param fileMd5             文件md5值
     * @param uploadFileParamsDto 上传文件的信息
     * @param bucket              桶
     * @param objectName          对象名称
     * @description 将文件信息添加到文件表
     */
    @Override
    @Transactional
    public BizFiles addMediaFilesToDb(String fileMd5, UploadFileVo uploadFileParamsDto, String bucket, String objectName) {
        BizFiles mediaFile = currentProxy.getMediaFileByFileMd5(fileMd5);
        if (mediaFile == null) {
            mediaFile = new BizFiles();
            //拷贝基本信息
            BeanUtils.copyProperties(uploadFileParamsDto, mediaFile);
            mediaFile.setFileMd5(fileMd5);
            mediaFile.setPreviewUrl("/" + bucket + "/" + objectName);
            mediaFile.setBucket(bucket);
            mediaFile.setStoragePath(objectName);
            //上传用户
            mediaFile.setUserId(uploadFileParamsDto.getUserId());
            //保存文件信息到文件表
            int insert = bizFilesMapper.insert(mediaFile);
            if (insert < 0) {
                log.error("保存文件信息到数据库失败,{}", mediaFile.toString());

            }
            log.debug("保存文件信息到数据库成功,{}", mediaFile.toString());

        }
        return mediaFile;

    }

    @Override
    @Transactional
    public BizFiles getMediaFileByFileMd5(String fileMd5) {
        //从数据库查询文件
        LambdaQueryWrapper<BizFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BizFiles::getFileMd5, fileMd5);
        return bizFilesMapper.selectOne(queryWrapper);
    }

    @Override
    public Result<Boolean> deleteFile(String fileMd5) {
        //查询文件信息
        BizFiles mediaFile = currentProxy.getMediaFileByFileMd5(fileMd5);
        if (mediaFile == null) {
            throw new BizException(ResultCode.PARAM_IS_NULL);
        }
        String objectName = mediaFile.getStoragePath();
        // 从minio中删除
        RemoveObjectArgs args = RemoveObjectArgs.builder().bucket(mediaFile.getBucket()).object(objectName).build();
        try {
            minioClient.removeObject(args);
        } catch (Exception e) {
            log.info("删除文件,文件不存在,文件md5:{}", fileMd5, e);
            throw new BizException(ResultCode.PARAM_IS_NULL);
        }
        //删除文件
        int flag = bizFilesMapper.deleteById(mediaFile.getId());
        if (flag == 0) {
            throw new BizException(ResultCode.PARAM_IS_NULL);
        }
        return Result.success(true);
    }

    @Override
    public Result<Boolean> checkFile(String fileMd5) {
        //查询文件信息
        BizFiles mediaFile = currentProxy.getMediaFileByFileMd5(fileMd5);
        if (mediaFile != null) {
            //桶
            String bucket = mediaFile.getBucket();
            //存储目录
            String filePath = mediaFile.getStoragePath();
            //文件流
            InputStream stream = null;
            try {
                stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filePath).build());

                if (stream != null) {
                    //文件已存在
                    return Result.success(true);
                }
            } catch (Exception e) {
                log.info("校验文件是否存在,文件不存在,文件md5:{}", fileMd5, e);
            }
        }
        //文件不存在
        return Result.success(false);
    }

    /**
     * 校验分块文件是否存在，返回不存在的分块号列表
     */
    @Override
    public Result<Boolean> checkChunk(String fileMd5, int chunk) {

        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);

        String chunkFilePath = chunkFileFolderPath + chunk;
        InputStream inputStream = null;
        try {
            // 方式1：使用statObject()轻量校验（推荐，仅检查元数据）
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketVideo).object(chunkFilePath).build());
        } catch (Exception e) {
            // 存在
            return Result.success(false);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ex) {
                    log.error("关闭分块输入流失败", ex);
                }
            }
        }
        // 不存在
        return Result.success(true);
    }

    @Override
    public Result uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {

        //得到分块文件的目录路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunk;
        //mimeType
        String mimeType = getMimeType(null);
        //将文件存储至minIO
        boolean b = addMediaFilesToMinio(localChunkFilePath, mimeType, bucketVideo, chunkFilePath);
        if (!b) {
            log.debug("上传分块文件失败:{}", chunkFilePath);
            return Result.failed("上传分块失败");
        }
        log.debug("上传分块文件成功:{}", chunkFilePath);
        return Result.success(true);
    }


    @Override
    public Result mergeChunks(String fileMd5, int chunkTotal, UploadFileVo uploadFileParamsDto) {
        //获取分块文件路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        //组成将分块文件路径组成 List<ComposeSource>
        List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> ComposeSource.builder().bucket(bucketVideo).object(chunkFileFolderPath.concat(Integer.toString(i))).build()).collect(Collectors.toList());
        //=====合并=====
        //文件名称
        String fileName = uploadFileParamsDto.getFileName();
        //文件扩展名
        String extName = fileName.substring(fileName.lastIndexOf("."));
        //合并文件路径
        String mergeFilePath = getFilePathByMd5(fileMd5, extName);
        ObjectWriteResponse response = null;
        try {
            //合并文件
            response = minioClient.composeObject(ComposeObjectArgs.builder().bucket(bucketVideo).object(mergeFilePath).sources(sourceObjectList).build());
            log.debug("合并文件成功:{}", mergeFilePath);
        } catch (Exception e) {
            log.debug("合并文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
            return Result.failed("合并文件失败。");
        }
//        String etag = response.etag();
//        if (!etag.equals(fileMd5)) {
//            return ResultUtils.error("合并文件失败。");
//        }
        // TODO 校验合并文件（在考虑下方式）
        // 验证md5是否和源文件的md5值一样
        // 下载合并之后的文件
        File minioFile = downloadFileFromMinIo(bucketVideo, fileMd5, mergeFilePath);
        if (minioFile == null) {
            log.debug("下载合并后文件失败,mergeFilePath:{}", mergeFilePath);
            return Result.failed("下载合并后文件失败。");
        }
        // 计算md5值
        try (InputStream newFileInputStream = new FileInputStream(minioFile)) {
            //minio上文件的md5值
            String md5Hex = DigestUtil.md5Hex(newFileInputStream);
            //比较md5值，不一致则说明文件不完整
            if (!fileMd5.equals(md5Hex)) {
                return Result.failed("文件合并校验失败，最终上传失败。");
            }
            //文件大小
            uploadFileParamsDto.setFileSize(minioFile.length());
        } catch (Exception e) {
            log.debug("校验文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
            return Result.failed("文件合并校验失败，最终上传失败。");
        } finally {
            if (minioFile != null) {
                minioFile.delete();
            }
        }
        //文件入库
        BizFiles bizMediaFile = currentProxy.addMediaFilesToDb(fileMd5, uploadFileParamsDto, bucketVideo, mergeFilePath);

        //=====清除分块文件=====
        clearChunkFiles(chunkFileFolderPath, chunkTotal, bucketVideo);
        return Result.success(true);
    }


    /**
     * 从minio下载文件
     *
     * @param bucketVideo   桶
     * @param mergeFilePath 合并后的文件路径
     * @return
     */
    private File downloadFileFromMinIo(String bucketVideo, String fileMd5, String mergeFilePath) {
        //临时文件
        File minioFile = null;
        FileOutputStream outputStream = null;
        try {
            InputStream stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucketVideo).object(mergeFilePath).build());
            minioFile = new File(Paths.get(SystemConstants.TEMP_DIR).toAbsolutePath() + "/minio_" + fileMd5 + ".merge");
            outputStream = new FileOutputStream(minioFile);
            IOUtils.copy(stream, outputStream);
            return minioFile;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 得到合并后的文件的地址
     *
     * @param fileMd5 文件id即md5值
     * @param fileExt 文件扩展名
     */
    private String getFilePathByMd5(String fileMd5, String fileExt) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }

    //得到分块文件的目录
    public String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    /**
     * 根据文件扩展名获取对应的 MIME 类型（如 "txt" → "text/plain"）
     * 如果无法识别扩展名，默认返回通用的二进制流类型（application/octet-stream）
     *
     * @param extension 文件扩展名（不带点，如 "png"、"txt"），可为 null 或空字符串
     */
    private String getMimeType(String extension) {
        if (StrUtil.isBlank(extension)) {
            return MediaType.APPLICATION_OCTET_STREAM_VALUE;
        }
        String mimeType = FileUtil.getMimeType("file." + extension);
        return mimeType != null ? mimeType : MediaType.APPLICATION_OCTET_STREAM_VALUE;
    }

    /**
     * 计算文件的 MD5 哈希值（用于文件唯一性校验或指纹识别）
     *
     * @param file 要计算 MD5 的文件对象
     * @return 返回文件的 MD5 哈希值（32位小写字符串），如果计算失败则返回 null
     * @throws RuntimeException 如果文件读取失败或计算 MD5 时发生异常
     */
    private String getFileMd5(File file) {
        try {
            return DigestUtil.md5Hex(file);
        } catch (Exception e) {
            log.error("计算文件 MD5 哈希值失败,文件路径:{}", file.getAbsolutePath(), e);
            return null;
        }
    }

    /**
     * 获取默认的文件存储目录路径，格式为：年/月/日/
     * 例如：2023/10/25/
     *
     * @return 返回格式化后的日期路径字符串，如 "2023/10/25/"
     */
    private String getDefaultFolderPath() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(new Date()).replace("-", "/") + "/";
    }

    /**
     * 清除分块文件
     *
     * @param chunkFileFolderPath 分块文件路径（建议以分隔符结尾，如"/"）
     * @param chunkTotal          分块文件总数
     * @param bucket              存储桶名称
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal, String bucket) {
        // 校验参数合法性，这里用了 Hutool 的 StrUtil，也可以自己判空
        if (chunkTotal <= 0 || StrUtil.isEmpty(chunkFileFolderPath) || StrUtil.isEmpty(bucket)) {
            log.warn("清除分块文件参数不合法: chunkFileFolderPath={}, chunkTotal={}, bucket={}", chunkFileFolderPath, chunkTotal, bucket);
            return;
        }

        try {
            // 统一处理文件夹路径，确保以 / 结尾，方便拼接分块序号路径
            String normalizedFolderPath = chunkFileFolderPath.endsWith("/") ? chunkFileFolderPath : chunkFileFolderPath + "/";

            // 构建所有需要删除的分块文件路径，格式：规范化后的文件夹路径 + 分块序号
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> i + 1).limit(chunkTotal).map(i -> new DeleteObject(normalizedFolderPath + i)).collect(Collectors.toList());

            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(bucket).objects(deleteObjects).build();

            Iterable<io.minio.Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);

            // 处理删除结果，记录每个分块的删除情况
            for (io.minio.Result<DeleteError> result : results) {
                try {
                    DeleteError error = result.get();
                    log.error("分块文件删除失败: objectName={}, errorCode={}, message={}", error.objectName(), error.code(), error.message());
                } catch (Exception e) {
                    log.error("处理分块文件删除结果时发生异常", e);
                }
            }

            log.info("分块文件清理完成, 路径: {}, 总数: {}", normalizedFolderPath, chunkTotal);

        } catch (Exception e) {
            log.error("清除分块文件失败, chunkFileFolderPath: {}, chunkTotal: {}", chunkFileFolderPath, chunkTotal, e);
        }
    }

    private void createMqMessage(String fileMd5, int totalChunks) {
        BizMqMessage bizMqMessage = new BizMqMessage();
        bizMqMessage.setMessageId(fileMd5);
        bizMqMessage.setBusinessId(fileMd5);
        bizMqMessage.setExchange(RabbitMqConstants.MINIO_MERGE_EXCHANGE);
        bizMqMessage.setRoutingKey(RabbitMqConstants.MINIO_MERGE_ROUTING_KEY);
        bizMqMessage.setQueue(RabbitMqConstants.MINIO_MERGE_QUEUE);
        bizMqMessage.setCount(totalChunks);
        bizMqMessage.setRealCount(0);
        bizMqMessageMapper.insert(bizMqMessage);
    }

}




